/*
 * XDefaultFactory.java, created on Oct 23, 2005
 * 
 * Copyright (c) 2005 - 2007
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xfc;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.beans.PropertyEditorSupport;
import java.lang.reflect.InvocationTargetException;

import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xfc.components.XDefaultAction;
import org.xfc.components.XProxyAction;
import org.xfc.util.XResources;

/**
 *
 *
 * @author Devon Carew
 */
public class XDefaultComponentFactory
	implements XComponentFactory
{
	private static Log log = LogFactory.getLog(XDefaultComponentFactory.class);
	
	protected XApp app;
	
	static
	{
        PropertyEditorManager.registerEditor(Character.class, CharEditor.class);
        PropertyEditorManager.registerEditor(Character.TYPE, CharEditor.class);
        PropertyEditorManager.registerEditor(Dimension.class, DimensionEditor.class);
        PropertyEditorManager.registerEditor(Icon.class, IconEditor.class);
        PropertyEditorManager.registerEditor(Image.class, ImageEditor.class);
        PropertyEditorManager.registerEditor(KeyStroke.class, KeyStrokeEditor.class);
        
        // TODO: our own integer editor (string editor also?)
        // Register the Sun editors that Sun didn't fully register.
        PropertyEditorManager.registerEditor(Integer.class, IntEditor.class);
	}
	
	/**
	 * @param app
	 */
	public XDefaultComponentFactory(XApp app)
	{
		this.app = app;
	}
	
	public XProxyAction createAction(String actionCommand)
	{
		return new XProxyAction(actionCommand);
	}
	
	public XDefaultAction applyAction(XDefaultAction action)
	{
		return (XDefaultAction)apply(action, action.getCommand());	
	}
	
	public JButton createButton(Action action)
	{
		return new JButton(action);
	}

	public JToolBar createToolBar()
	{
		JToolBar toolBar = new JToolBar();
		
		toolBar.setFloatable(false);
		
		return toolBar;
	}
	
	public JMenuBar createMenuBar()
	{
		return new JMenuBar();
	}
	
	public JMenu createMenu()
	{
		return new JMenu();
	}
	
	public JMenu applyMenu(JMenu menu)
	{
		return (JMenu)apply(menu);
	}
	
	/**
	 * @param component
	 * @return the given component
	 */
	protected JComponent apply(Component component)
	{
		return (JComponent)apply(component, component.getName());
	}
	
	/**
	 * @param object
	 * @param root
	 * @return the given object
	 */
	protected Object apply(Object object, String root)
	{
		try
		{
			BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass());
			
			PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
			
			for (int i = 0; i < descriptors.length; i++)
			{
				PropertyDescriptor descriptor = descriptors[i];
				
				if (descriptor.getWriteMethod() == null)
					continue;
				
				String propName = descriptor.getName();
				
				String value = app.getString(root + "." + propName);
				
				if (value != null)
				{
					Class propertyType = descriptor.getPropertyType();
					
					if (propertyType.isAssignableFrom(String.class))
					{
						descriptor.getWriteMethod().invoke(object, new Object[] { value });
					}
					else
					{
						Object objValue = convertValue(propertyType, propName, value);
						
						if (objValue != null)
							descriptor.getWriteMethod().invoke(object, new Object[] { objValue });
					}
				}
			}
		}
		catch (IllegalAccessException iae)
		{
			log.warn(iae);
		}
		catch (InvocationTargetException ite)
		{
			log.warn(ite);
		}
		catch (IntrospectionException ie)
		{
			log.warn(ie);
		}
		
		return object;
	}
	
	/**
	 * @return the resource for this factory's application
	 */
	protected XResources getResources()
	{
		return app.getResources();
	}
	
	public Border createSubtleComponentBorder()
	{
//		Border border = UIManager.getBorder("ToolBar.border");
//		
//		if (border == null || border.getClass().getName().indexOf(".metal.") != -1)
//			return BorderFactory.createLoweredBevelBorder();
//		
//		return border;
		
		// TODO: -
		return new CompoundBorder(
			BorderFactory.createMatteBorder(1, 0, 0, 0, Color.GRAY),
			BorderFactory.createEmptyBorder(2, 0, 0, 0));
		
		//return BorderFactory.createEmptyBorder();
		//return new JMenuBar().getBorder();
		//return BorderFactory.createLoweredBevelBorder();
	}
	
	/**
	 * Convert the property string to a value object.
	 * 
	 * @param valueType
	 * @param propertyName
	 * @param stringValue
	 * @return the converted value, or <code>null</code> if an error occured
	 */
	protected Object convertValue(Class valueType, String propertyName, String stringValue)
    {
        try
        {
            PropertyEditor editor = getEditor(valueType, propertyName);
            
            if (editor == null)
            {
                log.warn("Have value for " + propertyName + " of type " + valueType.getName() + " but no editor");
                return null;
            }

            editor.setAsText(stringValue);
            
            Object object = editor.getValue();
            
            if (object == null)
                log.warn("String value '" + stringValue + "' of type " + valueType.getName() + " converted to null");
            
            return object;
        }
        catch (Exception e)
        {
            log.warn("Improperly formed value for " + propertyName, e);
            return null;
        }
    }

    private PropertyEditor getEditor(Class valueType, String valueName)
    {
        // TODO: generalize editor overrides
        if (valueName.indexOf("nemonic") > 0)
            return new MnemonicEditor();
        
        // TODO: cache editor instances since findEditor returns a new one
        return PropertyEditorManager.findEditor(valueType);
    }
	
    
    // The property editors.
    
    /**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and a <code>Character</code>.
     */
    public static class CharEditor
		extends PropertyEditorSupport
	{
		public CharEditor()
		{

		}

		public void setAsText(String text)
			throws IllegalArgumentException
		{
			if (text == null || text.length() == 0)
				setValue(null);
			else
				setValue(new Character(text.charAt(0)));
		}
	}
	
	/**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and a <code>Dimension</code>. The string should be of the
	 * form <i>width</i>x<i>height</i>.
	 */
	public static class DimensionEditor
		extends PropertyEditorSupport
	{
		public DimensionEditor()
		{
			
		}
		
		public void setAsText(String text)
			throws IllegalArgumentException
		{
			if (text == null || text.length() == 0)
			{
				setValue(null);
				return;
			}

			int xIndex = text.indexOf('x');
			
			if (xIndex <= 0)
				throw new IllegalArgumentException("Invalid dimension: " + text);

			String width = text.substring(0, xIndex);
			String height = text.substring(xIndex + 1);
			
			if (width.length() == 0 || height.length() == 0)
				throw new IllegalArgumentException("Invalid dimension: " + text);
			
			try
			{
				setValue(new Dimension(Integer.parseInt(width), Integer.parseInt(height)));
			}
			catch (NumberFormatException nfe)
			{
				throw new IllegalArgumentException("Invalid dimension: " + text + "(" + nfe + ")");
			}
		}
	}
	
	/**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and an <code>Integer</code>.
	 */
    public static class IntEditor
		extends PropertyEditorSupport
	{
		public IntEditor()
		{
			
		}
	
		public void setAsText(String text)
			throws IllegalArgumentException
		{
			if (text == null || text.length() == 0)
				setValue(null);
			else
				setValue(new Integer(text));
		}
	}
	

	/**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and a <code>Double</code>.  Sun provides a similar
	 * editor but does not register it for use with the <code>Double</code> class
	 * only with <code>native</code>.
	 */
	public static class DoubleEditor
		extends PropertyEditorSupport
	{
		public DoubleEditor()
		{
			
		}
		
	    public void setAsText(String text)
	    	throws IllegalArgumentException
	    {
	        setValue(Double.valueOf(text));
	    }
	}
	

	/**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and an <code>Icon</code>.
	 */
	public static class IconEditor
		extends PropertyEditorSupport
	{
		public IconEditor()
		{
			
		}
		
	    public void setAsText(String text)
	    	throws IllegalArgumentException
	    {
	        if (text == null || text.length() == 0)
	            setValue(null);
	        else
	            setValue(XApp.getApp().getResources().getIcon(text));
	    }
	}


	/**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and an <code>Icon</code>.
	 */
	public static class ImageEditor
		extends PropertyEditorSupport
	{
		public ImageEditor()
		{
			
		}

	    public void setAsText(String text)
	    	throws IllegalArgumentException
	    {
	        if (text == null || text.length() == 0)
	        {
	            setValue(null);
	        }
	        else
	        {
	            ImageIcon imageIcon = XApp.getApp().getResources().getIcon(text);
	            
	            setValue(imageIcon == null ? null : imageIcon.getImage());
	        }
	    }
	}


	/**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and a <code>KeyStroke</code>. The string should be of the
	 * form <i>modifiers</i>+<i>shortcut</i> where <i>modifiers</i> is any combination
	 * of A for Alt, C for Control, S for Shift or M for Meta, and <i>shortcut</i> is
	 * either a single character, or a keycode name from the <code>KeyEvent</code>
	 * class, without the <code>VK_</code> prefix.
	 */
	public static class KeyStrokeEditor
		extends PropertyEditorSupport
	{
		public KeyStrokeEditor()
		{

		}

		public void setAsText(String text)
			throws IllegalArgumentException
		{
			if (text == null || text.length() == 0)
			{
				setValue(null);
				return;
			}

			int modifiers = 0;
			int index = text.indexOf('+');
			
			if (index != -1)
			{
				for (int i = 0; i < index; i++)
				{
					switch (Character.toUpperCase(text.charAt(i)))
					{
						case 'A':
							modifiers |= InputEvent.ALT_MASK;
							break;
						case 'C':
							modifiers |= InputEvent.CTRL_MASK;
							break;
						case 'M':
							modifiers |= InputEvent.META_MASK;
							break;
						case 'S':
							modifiers |= InputEvent.SHIFT_MASK;
							break;
						case 'P':
							modifiers |= Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
							break;
						default:
						    throw new IllegalArgumentException("Invalid key modifier: " + text.charAt(i));
					}
				}
			}

			int 	ch = '\0';
			String 	key = text.substring(index + 1);
			
			if (key.length() == 1)
			{
				ch = Character.toUpperCase(key.charAt(0));
			}
			else if (key.length() == 0)
			{
				throw new IllegalArgumentException("Invalid key stroke: " + text);
			}
			else
			{
				try
				{
					ch = KeyEvent.class.getField("VK_".concat(key)).getInt(null);
				}
				catch (Exception e)
				{
					throw new IllegalArgumentException("Invalid key stroke: " + text);
				}
			}

			setValue(KeyStroke.getKeyStroke(ch, modifiers));
		}
	}


	/**
	 * A <code>PropertyEditor</code> for converting back and forth between a
	 * <code>String</code> and an <code>int</code> representing a character.
	 */
	public static class MnemonicEditor
		extends PropertyEditorSupport
	{
	    public void setAsText(String text)
	    	throws IllegalArgumentException
	    {
	        if (text == null || text.length() == 0)
	            setValue(null);
	        else
	            setValue(new Integer(text.charAt(0)));
	    }
	}

}
